import 'package:meta/meta.dart';
import 'package:angular_compiler/v1/src/compiler/expression_parser/parser.dart';
import 'package:angular_compiler/v1/src/compiler/identifiers.dart';
import 'package:angular_compiler/v1/src/compiler/ir/model.dart' as ir;
import 'package:angular_compiler/v1/src/compiler/output/output_ast.dart' as o;
import 'package:angular_compiler/v1/src/compiler/schema/element_schema_registry.dart';
import 'package:angular_compiler/v1/src/compiler/template_ast.dart'
    show templateVisitAll;
import 'package:angular_compiler/v1/cli.dart';
import 'package:angular_compiler/v1/src/metadata.dart';

import 'compile_element.dart' show CompileElement;
import 'compile_view.dart' show CompileView;
import 'view_binder.dart' show bindView;
import 'view_builder.dart';
import 'view_compiler_utils.dart' show getHostViewFactoryName;

class ViewCompileResult {
  List<o.Statement> statements;
  ViewCompileResult(this.statements);
}

/// Compiles a single component to a set of CompileView(s) and generates top
/// level statements to support debugging and view factories.
///
/// - Creates main CompileView
/// - Runs ViewBuilderVisitor over template ast nodes
///     - For each embedded template creates a child CompileView and recurses.
/// - Builds a tree of CompileNode/Element(s)
class ViewCompiler {
  final CompilerFlags _genConfig;
  final ElementSchemaRegistry _schemaRegistry;
  Parser parser;

  ViewCompiler(this._genConfig, this.parser, this._schemaRegistry);

  ViewCompileResult compileComponent(
    ir.View view,
    o.Expression styles,
    Map<String, String> deferredModules, {
    @required bool registerComponentFactory,
  }) {
    var statements = <o.Statement>[];
    var compileView = CompileView(
      view.cmpMetadata,
      _genConfig,
      view.directiveTypes,
      view.pipes,
      styles,
      0,
      CompileElement.root(),
      [],
      deferredModules,
    );
    view.compileView = compileView;
    _buildView(view);
    // Need to separate binding from creation to be able to refer to
    // variables that have been declared after usage.
    bindView(view, _schemaRegistry, bindHostProperties: true);
    _finishView(compileView, statements,
        registerComponentFactory: registerComponentFactory);
    return ViewCompileResult(statements);
  }

  /// Builds the view and returns number of nested views generated.
  void _buildView(ir.View view) {
    var builderVisitor = ViewBuilderVisitor(view.compileView);
    templateVisitAll(
      builderVisitor,
      view.parsedTemplate,
      view.compileView.declarationElement,
    );
    view.compileView.providers = builderVisitor.providers;
  }

  /// Creates top level statements for main and nested views generated by
  /// buildView.
  void _finishView(CompileView view, List<o.Statement> targetStatements,
      {@required bool registerComponentFactory}) {
    view.afterNodes();
    _createViewTopLevelStmts(view, targetStatements,
        registerComponentFactory: registerComponentFactory);
    var nodeCount = view.nodes.length;
    var nodes = view.nodes;
    for (var i = 0; i < nodeCount; i++) {
      var node = nodes[i];
      if (node is CompileElement && node.embeddedView != null) {
        _finishView(
          node.embeddedView,
          targetStatements,
          registerComponentFactory: false,
        );
      }
    }
  }

  void _createViewTopLevelStmts(
    CompileView view,
    List<o.Statement> targetStatements, {
    @required bool registerComponentFactory,
  }) {
    final viewClass = createViewClass(view, parser);
    targetStatements.add(viewClass);
    if (view.viewType != ViewType.component) {
      // View factories are only needed for embedded and host views, to be used
      // by `TemplateRef` and `ComponentFactory` respectively.
      targetStatements.add(createViewFactory(view, viewClass));
    }
    targetStatements.addAll(
        registerComponentFactory ? _registerComponentFactory(view) : []);
  }

  /// Returns statements that declare a component factory for [view].
  ///
  /// These component factories are registered in `initReflector()` and used in
  /// user code.
  List<o.Statement> _registerComponentFactory(CompileView view) {
    final componentTypeMetadata = view.component.type;
    final componentType = o.importType(componentTypeMetadata);
    final componentName = componentTypeMetadata.name;
    final componentFactoryVar = o.variable('_${componentName}NgFactory');
    final componentFactoryArgs = [
      o.literal(view.component.selector),
      o.variable(getHostViewFactoryName(view.component)),
    ];

    // Declares a component factory.
    //
    //  const _FooComponentNgFactory = ComponentFactory<FooComponent>(
    //      'foo', viewFactory_FooComponentHost0);
    //
    // The explicit constructor type argument is necessary to prevent inferring
    // a more specific type from any generic type parameter bounds on the view
    // factory which would clash with the type returned by the public component
    // factory getter (defined below).
    final declareComponentFactory = componentFactoryVar
        .set(o.importExpr(
      Identifiers.ComponentFactory,
      typeParams: [componentType],
    ).instantiate(componentFactoryArgs))
        .toDeclStmt(null, [o.StmtModifier.Const]);

    // Declares a getter to access the component factory.
    //
    //  ComponentFactory<FooComponent> get FooComponentNgFactory =>
    //      _FooComponentNgFactory;
    //
    // This getter is used to hide the fact that the component factory is const
    // so that users aren't tempted to provide it as a value.
    final declareGetComponentFactory = o.fn(
      [], // No parameters.
      [o.ReturnStatement(componentFactoryVar)],
      o.importType(Identifiers.ComponentFactory, [componentType]),
    ).toGetter('${componentName}NgFactory');

    // Declares a function to create a component factory.
    //
    //  ComponentFactory<FooComponent<T>> createFooComponentFactory<T>() {
    //    return ComponentFactory('foo', viewFactory_FooComponentHost0);
    //  }
    //
    // This allows users to specify generic type arguments to the component
    // factory.
    final declareCreateComponentFactory = o.fn(
      [], // No parameters.
      [
        o.ReturnStatement(o
            .importExpr(Identifiers.ComponentFactory)
            .instantiate(componentFactoryArgs)),
      ],
      o.importType(Identifiers.ComponentFactory, [contextType(view)]),
    ).toDeclStmt(
      'create${componentName}Factory',
      typeParameters: view.component.originType.typeParameters,
    );

    return [
      declareComponentFactory,
      declareGetComponentFactory,
      declareCreateComponentFactory,
    ];
  }
}
